package fr.limsi.seven;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;

import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;

/**
 * Link2D.java Created on : 29 mai 07
 */

/**
 * @author Camille
 */
public class Link2D extends JPanel {

	/**
	 * Auto-generated serial version UID number
	 */
	private static final long serialVersionUID = -7186946149387225215L;

	/**
	 * Link shape
	 */
	public static final int TRIANGULAR = Link.TRIANGULAR;

	/**
	 * Link shape
	 */
	public static final int TRAPEZOIDAL = Link.TRAPEZOIDAL;

	/**
	 * Triangular link connection point epsilon (error correction for trigonometric approximation)
	 */
	public static final float TRIANGULAR_EPSILON = 1.5f;

	/**
	 * Link direction
	 */
	public static final int LEFT = 0;

	/**
	 * Link direction
	 */
	public static final int RIGHT = 1;

	/**
	 * Incurvation (in pixel) for {@link CubicCurve2D}
	 */
	public final float INCURVATION = 4;

	/**
	 * Trapezoid top edge width delta (in pixel) ratio to the bottom edge
	 */
	public static final int TRAPEZOID_DELTA = 6;

	/**
	 * Shape
	 */
	protected int shape;

	/**
	 * Link bounding shape (used by <i>contains()</i> functions
	 */
	protected GeneralPath linkBoundingShape;

	/**
	 * Minimum height to reach for the link when its shape is <i>TRIANGULAR</i>
	 */
	protected int minHeight;

	/**
	 * Focus indicator
	 */
	protected boolean focused;

	/**
	 * {@link Pyramide2D} owner
	 */
	protected Pyramide2D pyramide2D;

	/**
	 * Linked parent
	 */
	protected Node2D parent;

	/**
	 * Linked child
	 */
	protected Node2D child;

	/**
	 * User label
	 */
	protected Label2D userLabel;

	/**
	 * Default constructor
	 * 
	 * @param parent
	 *            the linked parent (a {@link Node2D})
	 * @param child
	 *            the linked child (a {@link Node2D})
	 */
	public Link2D(Pyramide2D view, Node2D parent, Node2D child) {
		this.pyramide2D = view;
		this.parent = parent;
		this.child = child;

		userLabel = new Label2D("", Label2D.LINK);

		shape = TRIANGULAR;
		focused = false;
		linkBoundingShape = new GeneralPath();

		buildInteractions();
	}

	@Override
	protected void paintComponent(Graphics g) {

		Graphics2D g2 = (Graphics2D) g;

		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);

		if (!focused) {
			g2.setPaint(parent.getColor());
			g2.setStroke(parent.getStroke());
		}
		else {
			g2.setPaint(new Color(parent.getColor().getRed(), parent.getColor().getGreen(), parent.getColor().getBlue()));
			g2.setStroke(Pyramide.FOCUS_STROKE);
		}

		traceLink(g2);

		/* User label displaying / hiding */
		if (!userLabelOverflows()) {
			showUserLabel();
		}
		else {
			if (!focused) {
				hideUserLabel();
			}
		}
	}

	/**
	 * Creates and sets the link mouse listeners
	 */
	protected void buildInteractions() {

		final Link2D link = this;

		MouseAdapter mA = new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {

				pyramide2D.requestFocus();

				pyramide2D.getSearchTool().setSearching(false);
				pyramide2D.repaint();

				if (e.getButton() == MouseEvent.BUTTON1 && !pyramide2D.getShelf().isShowing()) {

					if ((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) {
						pyramide2D.toggleSecondarySelection(link.getParentNode());
					}
					else {
						pyramide2D.togglePrimarySelection(link.getParentNode());
					}

					pyramide2D.repaint();
				}

				if (e.getButton() == MouseEvent.BUTTON3 && !pyramide2D.getShelf().isShowing()) {
					link.showPopupMenu();
				}
			}

			@Override
			public void mouseEntered(MouseEvent e) {

				if (!pyramide2D.getShelf().isShowing() && !pyramide2D.getSearchTool().isSearching()) {

					setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
					focused = true;
					link.showUserLabel();
					pyramide2D.getAnimationMonitor().queueAnimation(pyramide2D.getAnimationBuilder().getColorAnimation(new Color(((float) pyramide2D.getDefaultDrawingColor().getRed()) / 255.0f, ((float) pyramide2D.getDefaultDrawingColor().getGreen()) / 255.0f, ((float) pyramide2D.getDefaultDrawingColor().getBlue()) / 255.0f, Pyramide2D.FADING)));
				}
			}

			@Override
			public void mouseExited(MouseEvent e) {
				if (!pyramide2D.getShelf().isShowing() && !pyramide2D.getSearchTool().isSearching()) {
					setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
					focused = false;
					pyramide2D.getAnimationMonitor().queueAnimation(pyramide2D.getAnimationBuilder().getColorAnimation(pyramide2D.getDefaultDrawingColor()));
				}
			}
		};

		addMouseListener(mA);
	}

	/**
	 * Resets the link bounds (call when the nodes move)
	 */
	public void resetBounds() {

		minHeight = Integer.MAX_VALUE;

		for (Link2D link : parent.getLinksOut()) {
			Node2D otherChild = link.getChildNode();
			minHeight = Math.min(minHeight, otherChild.absolutePosition.y);
		}

		Point parentPosition = new Point(parent.getAbsolutePosition().x, parent.getAbsolutePosition().y);
		pyramide2D.applyTransformations(parentPosition);

		Point childPosition = new Point(child.getAbsolutePosition().x, child.getAbsolutePosition().y);
		pyramide2D.applyTransformations(childPosition);

		int top, left, bottom, right;

		if (childPosition.x < parentPosition.x) { // Left link

			top = parentPosition.y;
			left = childPosition.x;
			bottom = childPosition.y + (int) (child.getNodeSize() * pyramide2D.getScale());
			right = parentPosition.x + (int) (parent.getNodeSize() * pyramide2D.getScale());

		}
		else { // Right link

			top = parentPosition.y;
			left = parentPosition.x;
			bottom = childPosition.y + (int) (child.getNodeSize() * pyramide2D.getScale());
			right = childPosition.x + (int) (child.getNodeSize() * pyramide2D.getScale());

		}

		setBounds(left, top, (right - left), (bottom - top));
		linkBoundingShape = getLinkBoundingShape();

		repaint();
	}

	/**
	 * Creates the link bounding shape
	 * 
	 * @return the created bounding shape {@link GeneralPath}
	 */
	protected GeneralPath getLinkBoundingShape() {
		int direction;

		if (child.getAbsolutePosition().x < parent.getAbsolutePosition().x) { // Left link
			direction = LEFT;
		}
		else { // Right link
			direction = RIGHT;
		}

		switch (shape) {

		case TRIANGULAR:
			return getTriangularLinkBoundingShape(direction);

		case TRAPEZOIDAL:
			return getTrapezoidalLinkBoundingShape(direction);

		default:
			return getTriangularLinkBoundingShape(direction);

		}
	}

	/**
	 * Creates the link boundig shape for triangular links
	 * 
	 * @param direction
	 *            a direction integer constant
	 * @return the created bounding shape {@link GeneralPath}
	 */
	protected GeneralPath getTriangularLinkBoundingShape(int direction) {

		GeneralPath boundingShape = new GeneralPath();

		switch (direction) {

		case LEFT:

			boundingShape.moveTo(getWidth() - (parent.getNodeSize() * pyramide2D.getScale() * 1.5f), (parent.getNodeSize() * pyramide2D.getScale()) / 2);
			boundingShape.lineTo((child.getNodeSize() * pyramide2D.getScale()) / 2, getHeight() - ((child.getNodeSize() * pyramide2D.getScale()) / 2));
			boundingShape.lineTo(child.getNodeSize() * pyramide2D.getScale() * 1.5f, getHeight() - ((child.getNodeSize() * pyramide2D.getScale()) / 2));
			boundingShape.lineTo(getWidth() - ((parent.getNodeSize() * pyramide2D.getScale()) / 2), (parent.getNodeSize() * pyramide2D.getScale()) / 2);

			break;

		case RIGHT:

			boundingShape.moveTo((parent.getNodeSize() * pyramide2D.getScale()) / 2, (parent.getNodeSize() * pyramide2D.getScale()) / 2);
			boundingShape.lineTo(getWidth() - (child.getNodeSize() * pyramide2D.getScale() * 1.5f), getHeight() - ((child.getNodeSize() * pyramide2D.getScale()) / 2));
			boundingShape.lineTo(getWidth() - ((child.getNodeSize() * pyramide2D.getScale()) / 2), getHeight() - ((child.getNodeSize() * pyramide2D.getScale()) / 2));
			boundingShape.lineTo(parent.getNodeSize() * pyramide2D.getScale() * 1.5f, (parent.getNodeSize() * pyramide2D.getScale()) / 2);

			break;

		}

		boundingShape.closePath();

		return boundingShape;
	}

	/**
	 * Creates the link boundig shape for trapezoidal links
	 * 
	 * @param direction
	 *            a direction integer constant
	 * @return the created bounding shape {@link GeneralPath}
	 */
	protected GeneralPath getTrapezoidalLinkBoundingShape(int direction) {

		GeneralPath boundingShape = new GeneralPath();

		switch (direction) {

		case LEFT:
			boundingShape.moveTo(getWidth() - ((parent.getNodeSize() * pyramide2D.getScale()) / 2), 0);
			boundingShape.lineTo(0, 0);
			boundingShape.lineTo(0, getHeight() - ((child.getNodeSize() * pyramide2D.getScale()) / 2));
			boundingShape.lineTo(child.getNodeSize() * pyramide2D.getScale(), getHeight() - ((child.getNodeSize() * pyramide2D.getScale()) / 2));
			boundingShape.lineTo(child.getNodeSize() * pyramide2D.getScale(), parent.getNodeSize() * pyramide2D.getScale());
			boundingShape.lineTo(getWidth() - ((parent.getNodeSize() * pyramide2D.getScale()) / 2), parent.getNodeSize() * pyramide2D.getScale());

			break;

		case RIGHT:

			boundingShape.moveTo((parent.getNodeSize() * pyramide2D.getScale()) / 2, 0);
			boundingShape.lineTo(getWidth(), 0);
			boundingShape.lineTo(getWidth(), getHeight() - ((child.getNodeSize() * pyramide2D.getScale()) / 2));
			boundingShape.lineTo(getWidth() - (child.getNodeSize() * pyramide2D.getScale()), getHeight() - ((child.getNodeSize() * pyramide2D.getScale()) / 2));
			boundingShape.lineTo(getWidth() - (child.getNodeSize() * pyramide2D.getScale()), parent.getNodeSize() * pyramide2D.getScale());
			boundingShape.lineTo((parent.getNodeSize() * pyramide2D.getScale()) / 2, parent.getNodeSize() * pyramide2D.getScale());

			break;

		}

		boundingShape.closePath();

		return boundingShape;
	}

	@Override
	public boolean contains(Point p) {
		return contains(p.x, p.y);
	}

	@Override
	public boolean contains(int x, int y) {
		return linkBoundingShape.contains(x, y);
	}

	/**
	 * Traces the link
	 * 
	 * @param g2
	 */
	protected void traceLink(Graphics2D g2) {

		int direction;

		if (child.getAbsolutePosition().x < parent.getAbsolutePosition().x) { // Left link
			direction = LEFT;
		}
		else { // Right link
			direction = RIGHT;
		}

		switch (shape) {

		case TRIANGULAR:
			traceTriangularLink(g2, direction);
			break;

		case TRAPEZOIDAL:
			traceTrapezoidalLink(g2, direction);
			break;

		default:
			traceTriangularLink(g2, direction);
		}
	}

	/**
	 * Call by <i>traceLink</i> when the link shape is equal to the <i>TRIANGULAR</i> constant
	 * 
	 * @param g2
	 * @param direction
	 *            the link direction (<i>LEFT</i> or <i>RIGHT</i>)
	 */
	protected void traceTriangularLink(Graphics2D g2, int direction) {

		Point parentConnection;
		Point childConnection;

		switch (direction) {

		case LEFT:

			parentConnection = new Point(getWidth() - (int) ((parent.getNodeSize() * pyramide2D.getScale()) * ((1 / 2) + (Math.sqrt(2) / 2))), (int) ((parent.getNodeSize() * pyramide2D.getScale()) * ((1 / 2) + (Math.sqrt(2) / 2))));
			childConnection = new Point((int) ((child.getNodeSize() * pyramide2D.getScale()) * ((1 / 2) + (Math.sqrt(2) / 2))), getHeight() - (int) ((child.getNodeSize() * pyramide2D.getScale()) * ((1 / 2) + (Math.sqrt(2) / 2))));

			if (child.absolutePosition.y <= minHeight) {
				g2.draw(new Line2D.Float(parentConnection.x - (TRIANGULAR_EPSILON * pyramide2D.getScale()), parentConnection.y + (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.x + (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.y - (TRIANGULAR_EPSILON * pyramide2D.getScale())));
			}
			else {
				g2.draw(new Line2D.Float(parentConnection.x - (TRIANGULAR_EPSILON * pyramide2D.getScale()), parentConnection.y + (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.x + (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.y - (TRIANGULAR_EPSILON * pyramide2D.getScale()) - ((child.getAbsolutePosition().y - minHeight) * pyramide2D.getScale())));
				g2.draw(curvedLink(RIGHT, INCURVATION, childConnection.x + (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.y - (TRIANGULAR_EPSILON * pyramide2D.getScale()) - ((child.getAbsolutePosition().y - minHeight) * pyramide2D.getScale()), childConnection.x + (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.y - (TRIANGULAR_EPSILON * pyramide2D.getScale())));
			}

			break;

		case RIGHT:

			parentConnection = new Point((int) (((parent.getNodeSize() * pyramide2D.getScale()) * ((1 / 2) + (Math.sqrt(2) / 2)))), (int) ((parent.getNodeSize() * pyramide2D.getScale()) * ((1 / 2) + (Math.sqrt(2) / 2))));
			childConnection = new Point(getWidth() - (int) ((child.getNodeSize() * pyramide2D.getScale()) * ((1 / 2) + (Math.sqrt(2) / 2))), getHeight() - (int) ((child.getNodeSize() * pyramide2D.getScale()) * ((1 / 2) + (Math.sqrt(2) / 2))));

			if (child.absolutePosition.y <= minHeight) {
				g2.draw(new Line2D.Float(parentConnection.x + (TRIANGULAR_EPSILON * pyramide2D.getScale()), parentConnection.y + (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.x - (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.y - (TRIANGULAR_EPSILON * pyramide2D.getScale())));
			}
			else {
				g2.draw(new Line2D.Float(parentConnection.x + (TRIANGULAR_EPSILON * pyramide2D.getScale()), parentConnection.y + (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.x - (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.y - (TRIANGULAR_EPSILON * pyramide2D.getScale()) - ((child.getAbsolutePosition().y - minHeight) * pyramide2D.getScale())));
				g2.draw(curvedLink(LEFT, INCURVATION, childConnection.x - (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.y - (TRIANGULAR_EPSILON * pyramide2D.getScale()) - ((child.getAbsolutePosition().y - minHeight) * pyramide2D.getScale()), childConnection.x - (TRIANGULAR_EPSILON * pyramide2D.getScale()), childConnection.y - (TRIANGULAR_EPSILON * pyramide2D.getScale())));
			}

			break;

		}
	}

	/**
	 * Constucts a curved line linking points (xa, ya) and (xb,yb)
	 * 
	 * @param direction
	 *            RIGHT or LEFT constant
	 * @param incurvation
	 *            the curve incurvation
	 * @param xa
	 *            curved line start x coordinate
	 * @param ya
	 *            curved line start y coordinate
	 * @param xb
	 *            curved line end x coordinate
	 * @param yb
	 *            curved line end y coordinate
	 * @return the {@link CubicCurve2D} object to be drawn
	 */
	protected CubicCurve2D.Float curvedLink(int direction, float incurvation, float xa, float ya, float xb, float yb) {
		float ctrlP1x = Math.max(xa, xb);

		switch (direction) {
		case LEFT:
			ctrlP1x -= incurvation;
			break;
		case RIGHT:
			ctrlP1x += incurvation;
			break;
		}

		float ctrlP2x = ctrlP1x;

		float d = (Math.abs(yb - ya) / 3);
		float ctrlP1y = Math.min(ya, yb) + d;
		float ctrlP2y = ctrlP1y + d;

		return new CubicCurve2D.Float(xa, ya, ctrlP1x, ctrlP1y, ctrlP2x, ctrlP2y, xb, yb);
	}

	/**
	 * Call by <i>traceLink</i> when the link shape is equal to the <i>TRAPEZOIDAL</i> constant
	 * 
	 * @param g2
	 * @param direction
	 *            the link direction (<i>LEFT</i> or <i>RIGHT</i>)
	 */
	protected void traceTrapezoidalLink(Graphics2D g2, int direction) {

		switch (direction) {

		case LEFT:
			g2.draw(new Line2D.Float(getWidth() - (parent.getNodeSize() * pyramide2D.getScale()), (parent.getNodeSize() * pyramide2D.getScale()) / 2, ((child.getNodeSize() * pyramide2D.getScale()) / 2) + TRAPEZOID_DELTA, (parent.getNodeSize() * pyramide2D.getScale()) / 2));
			g2.draw(new Line2D.Float(((child.getNodeSize() * pyramide2D.getScale()) / 2) + TRAPEZOID_DELTA, (parent.getNodeSize() * pyramide2D.getScale()) / 2, ((child.getNodeSize() * pyramide2D.getScale()) / 2), getHeight() - (child.getNodeSize() * pyramide2D.getScale())));
			break;

		case RIGHT:
			g2.draw(new Line2D.Float(parent.getNodeSize() * pyramide2D.getScale(), (parent.getNodeSize() * pyramide2D.getScale()) / 2, getWidth() - ((child.getNodeSize() * pyramide2D.getScale()) / 2) - TRAPEZOID_DELTA, (parent.getNodeSize() * pyramide2D.getScale()) / 2));
			g2.draw(new Line2D.Float(getWidth() - ((child.getNodeSize() * pyramide2D.getScale()) / 2) - TRAPEZOID_DELTA, (parent.getNodeSize() * pyramide2D.getScale()) / 2, getWidth() - ((child.getNodeSize() * pyramide2D.getScale()) / 2), getHeight() - (parent.getNodeSize() * pyramide2D.getScale())));
			break;

		}
	}

	/**
	 * Shows a {@link Link2D} user label
	 */
	public void showUserLabel() {
		if (!(userLabel.getText().equals(""))) {
			if (!userLabel.isShowing()) {
				pyramide2D.add(userLabel, 0);

				Rectangle2D labelBBox = userLabel.getFontMetrics(userLabel.getFont()).getStringBounds(userLabel.getText(), pyramide2D.getGraphics());

				int labelWidth = (int) labelBBox.getWidth();
				int labelHeight = (int) labelBBox.getHeight();

				userLabel.setBounds(getX() + (getWidth() / 2), getY() + ((getHeight() - labelHeight) / 2), labelWidth + Pyramide2D.LABEL_BORDERS, labelHeight + Pyramide2D.LABEL_BORDERS);

				pyramide2D.repaint();
			}
		}
	}

	/**
	 * Hides a {@link Link2D} user label (if the zoom level is too low)
	 */
	public void hideUserLabel() {
		if (userLabel.isShowing()) {
			pyramide2D.remove(userLabel);
		}
	}

	/**
	 * Verifies that the user label doesn't hides one of the node children
	 * 
	 * @return true if the user label hides one of the node children, or else false
	 */
	public boolean userLabelOverflows() {
		Rectangle2D labelBBox = userLabel.getFontMetrics(userLabel.getFont()).getStringBounds(userLabel.getText(), pyramide2D.getGraphics());

		int labelHeight = (int) labelBBox.getHeight();

		return (((child.getAbsolutePosition().y * pyramide2D.getScale()) - (parent.getAbsolutePosition().y + (parent.getNodeSize() * pyramide2D.getScale()))) <= (getY() + getHeight() + labelHeight + Pyramide2D.LABEL_BORDERS + Pyramide2D.LABEL_MARGINS));
	}

	/**
	 * Creates and shows the popup menu of a node
	 * 
	 * @param node
	 *            the popup menu owner
	 */
	public void showPopupMenu() {

		final Link2D link = this;
		final JPopupMenu popup = new JPopupMenu(parent.getPNode().getName() + " - " + child.getPNode().getName());

		JMenuItem labelizeItem = new JMenuItem("Labeliser");
		MouseAdapter labelizeListener = new MouseAdapter() {
			@Override
			public void mousePressed(MouseEvent e) {
				new Thread() {
					@Override
					public void run() {

						if (popup.isShowing()) {
							popup.setVisible(false);
						}

						link.labelize();
					}
				}.start();
			}
		};

		labelizeItem.addMouseListener(labelizeListener);

		popup.add(labelizeItem);
		popup.show(pyramide2D, MouseInfo.getPointerInfo().getLocation().x - pyramide2D.getLocationOnScreen().x, MouseInfo.getPointerInfo().getLocation().y - pyramide2D.getLocationOnScreen().y);
	}

	/**
	 * User labelization operation
	 */
	public void labelize() {
		String labelString = JOptionPane.showInputDialog(pyramide2D, "Entrez un label pour le lien " + parent.getPNode().getName() + " - " + child.getPNode().getName(), userLabel.getText());

		if (labelString != null) {
			userLabel.setHorizontalAlignment(JLabel.CENTER);
			userLabel.setText(labelString);
		}
	}

	/**
	 * Getter for the link child {@link Node2D}
	 * 
	 * @return the child {@link Node2D}
	 */
	public Node2D getChildNode() {
		return child;
	}

	/**
	 * Getter for the parent child {@link Node2D}
	 * 
	 * @return the parent {@link Node2D}
	 */
	public Node2D getParentNode() {
		return parent;
	}

	/**
	 * Getter for the link user label
	 * 
	 * @return the link {@link Label2D}
	 */
	public Label2D getUserLabel() {
		return userLabel;
	}

	/**
	 * Getter for the link shape
	 * 
	 * @return an integer constant corresponding to the link shape
	 */
	public int getShape() {
		return shape;
	}

	/**
	 * Setter for the link shape
	 * 
	 * @param shape
	 *            a link shape integer constant
	 */
	public void setShape(int shape) {
		this.shape = shape;
	}

	/**
	 * Set the link focus state
	 * 
	 * @param state
	 *            true if the link has focus, or else false
	 */
	public void setFocused(boolean state) {
		this.focused = state;
	}

	/**
	 * Getter for the link ID
	 * 
	 * @return the link id {@link String} (<i>"parentID-childID"</i>)
	 */
	public String getID() {
		return parent.getID() + "-" + child.getID();
	}

	/**
	 * Releases all the link resources
	 */
	public void dispose() {
		userLabel.dispose();
	}
}
